package com.gsi.webstore.platform.storefront.pipelet.orderprocess.basket;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.intershop.beehive.bts.capi.orderprocess.ProductLineItem;
import com.intershop.beehive.bts.pipelet.common.PipelineConstants;
import com.intershop.beehive.core.capi.common.RemoveException;
import com.intershop.beehive.core.capi.common.SystemException;
import com.intershop.beehive.core.capi.component.ComponentMgr;
import com.intershop.beehive.core.capi.log.Logger;
import com.intershop.beehive.core.capi.naming.NamingMgr;
import com.intershop.beehive.core.capi.pipeline.Pipelet;
import com.intershop.beehive.core.capi.pipeline.PipeletExecutionException;
import com.intershop.beehive.core.capi.pipeline.PipelineDictionary;
import com.intershop.beehive.core.capi.pipeline.PipelineInitializationException;
import com.intershop.beehive.core.capi.request.Request;
import com.intershop.beehive.core.capi.request.SessionMgr;
import com.intershop.beehive.foundation.quantity.Quantity;
import com.intershop.beehive.foundation.quantity.QuantityException;
import com.intershop.beehive.foundation.quantity.StaticQuantityFormat;
import com.intershop.beehive.foundation.util.Iterators;
import com.intershop.sellside.appbase.b2c.capi.basket.B2CBasketBO;
import com.intershop.sellside.appbase.b2c.capi.basket.PLISplitInfo;
import com.intershop.sellside.appbase.b2c.capi.basket.PLISplitInfoMap;
import com.intershop.sellside.appbase.b2c.capi.basket.PLISplitInfoMergeStrategy;
import com.intershop.sellside.appbase.b2c.capi.basket.PLISplitInfoSplitStrategy;
import com.intershop.sellside.appbase.b2c.capi.order.ProductLineItemBO;
import com.intershop.sellside.appbase.b2c.internal.basket.PLISplitInfoMapImpl;

/**
    This pipelet updates the baskets quantities. 
    
    <p>Input parameters are:
    <br>Basket to update
    <br>Quantity of each ProductLineItem to be updated as a String, lookup from the 
    pipeline dictionary under the key made up of the ProductLineItem UUID prefixed 
    with 'Quantity_'.
    
    <p>The pipelet can be configured to automatically remove ProductLineItems
    for which a zero or -ve update quantity is specified.
    <p>The pipelet can be configured to update either the quantity attribute, 
    selected quantity attribute or both.
    <p>The pipelet is designed to work on LineItemCtnr so that a subclass 
    UpdateOrderQuantities can be easily supported.

*/

public class UpdateBasketBOQuantities extends Pipelet
{
    /**
        prefix used to lookup quantity strings to parse
    */

    private static final String LOOKUP_PREFIX = "Quantity_";
        
    /**
        Pipelet can be configured to remove items with quantities of zero or less
    */

    protected boolean removeNonPositiveConfigured = true;
        
    /**
        Pipelet configured to update quantity attribute 
    */

    protected boolean updateQuantityConfigured = true;
        
    /**
        Pipelet configured to update selected quantity attribute 
    */

    protected boolean updateSelectedQuantityConfigured = false;

    /**
        sessionMgr is needed to get the current session
    */
    protected SessionMgr sessionMgr = null;
    
    /**
    componentMgr is needed to get the component for pli splitting
     */
    private ComponentMgr componentMgr = null;

    /**
     * Constant used to access the pipeline dictionary with key 'AdjustedItems'
     */
    public static final String DN_ADJUSTED_ITEMS = "AdjustedItems";

    /**
     * Constant used to access the pipeline dictionary with key 'RejectedItems'
     */
    public static final String DN_REJECTED_ITEMS = "RejectedItems";

    /**
     * Constant used to access pipelet configuration with key 'AdjustQuantities'
     * 
     * Configures whether adjust quantities or not.
     */
    public static final String CN_ADJUST_QUANTITIES = "AdjustQuantities";

    /**
     * Member attribute that holds the pipelet configuration value 'AdjustQuantities'
     * 
     * Configures whether adjust quantities or not.
     */
    private String cfg_adjustQuantities = "";

    /**
     * Pipelet can be configured to adjust quantities or not
     */
    protected boolean adjustQuantities = true;
    
    /**
     * Constant used to access the pipeline dictionary with key 'Basket'
     * 
     * Basket to be updated
     */
    public static final String DN_BASKET = "Basket";

    /**
     *  Initializes the pipelet.
     *  @exception PipelineInitializationException thrown if a required config 
     *             parameter is not specified
     */

    public void init() throws PipelineInitializationException, SystemException
    {        
        Hashtable configuration = getConfiguration();
        
        // store 'AdjustQuantities' config value in field variable
        cfg_adjustQuantities = (String)getConfiguration().get(CN_ADJUST_QUANTITIES);

        if (cfg_adjustQuantities == null)
        {
            removeNonPositiveConfigured = true;
            adjustQuantities = true;
        }
        else
        {
            adjustQuantities = cfg_adjustQuantities.equalsIgnoreCase("true");
        }
        

        String remove = (String) configuration.get(PipelineConstants.REMOVE_NON_POSITIVE_QUANTITY_ITEMS);

        if (remove == null)
        {
            Logger.warn(this, "PipeletConfigurationNotFound for REMOVE_NON_POSITIVE_QUANTITY_ITEMS");
            removeNonPositiveConfigured = true;
        }
        else
        {
            removeNonPositiveConfigured = remove.equalsIgnoreCase("true");
        }

        String updateMode = (String) configuration.get("UpdateMode");
        if ("SelectedQuantity".equals(updateMode)) 
        {
            updateSelectedQuantityConfigured = true;
            updateQuantityConfigured = false;
        }
        else if ("Both".equals(updateMode)) 
        {
            updateSelectedQuantityConfigured = updateQuantityConfigured = true;
        }
        else // default is "Quantity"
        {
            updateSelectedQuantityConfigured = false;
            updateQuantityConfigured = true;
        }

        sessionMgr = (SessionMgr) NamingMgr.getInstance().lookupManager(
                    SessionMgr.REGISTRY_NAME);
        
        componentMgr = NamingMgr.getManager(ComponentMgr.class);
        
     }

    /**
       Updates the basket.

       @param  dict The pipeline dictionary
       @return Always returns PIPELET_NEXT
       @exception PipeletExecutionException thrown if a mandatory input parameter 
                is missing
     */


    public int execute(PipelineDictionary dict)
        throws SystemException, PipeletExecutionException
    {        
        // lookup 'Basket' in pipeline dictionary
        B2CBasketBO basket = (B2CBasketBO)dict.get(DN_BASKET);
        if (null == basket)
        {
            throw new PipeletExecutionException("Mandatory input parameter 'Basket' not available in pipeline dictionary.");
        
        }
        
        String             quantityLookupKey, unparsedQuantity, unit;
        ProductLineItemBO    pli;        
        StaticQuantityFormat qm;
        Quantity           parsedQuantity;
        Set<ProductLineItemBO>    removablePLISet = new HashSet<ProductLineItemBO>();
        Iterator<? extends ProductLineItemBO>           pliIterator;

        qm = Request.getCurrent().getLocale().getQuantityFormat();

        // Iterate through the array of PLIs
        pliIterator = basket.getProductLineItems().iterator();
        
        Map<String, List<String>> adjustedItems = new HashMap<String, List<String>>(basket.getProductLineItemsCount());
        Map<String, List<String>> rejectedItems = new HashMap<String, List<String>>(basket.getProductLineItemsCount());
        while (pliIterator.hasNext())
        {
            // attempt to get the ProductLineItem identified
            pli = (ProductLineItemBO) pliIterator.next();

            // The previously set quantity unit is reused
            unit = pli.getQuantity()==null?"": pli.getQuantity().getUnit();

            // lookup the quantity string to parse
            quantityLookupKey = LOOKUP_PREFIX + pli.getID();
            unparsedQuantity  = (String) dict.get(quantityLookupKey);

            // If no quantity specified do nothing
            if (unparsedQuantity == null || (unparsedQuantity.length() == 0))
            {
                Logger.debug(this,"Quantity not found: " + quantityLookupKey );
                continue;
            }
            
            // check maximum length
            if (unparsedQuantity.length() > 30)
            {
                Logger.debug(this,"Quantity to large: " + unparsedQuantity );
                continue;
            }

            // Otherwise parse quantity string into Quantity instance
            try
            {
                parsedQuantity = qm.parse(unparsedQuantity, unit);
            }
            catch(ParseException ex)
            {
                Logger.debug(this,"Unable to parse Quantity: " + unparsedQuantity );
                continue;
            }
            
            // check if the quantity invalid and add pli:uuid to rejectedItems 
            try
            {
                if (isQuantityInvalid(parsedQuantity, pli) && !adjustQuantities)
                {
                    List<String> reasons=new ArrayList<String>(1);
                    reasons.add("InvalidQuantity");
                    rejectedItems.put(pli.getID(),reasons);
                }
            }
            catch (QuantityException qe)
            {
                Logger.debug(this,"QuantityException quantity "
                                + "for pli with quantity:" + pli.getQuantity() + " minorder:"
                                + pli.getMinOrderQuantity() + " step:" + pli.getStepQuantity());
                continue;
            }            
            
            
            // if configured to remove zero & negative quantities, record it
            if (removeNonPositiveConfigured && !parsedQuantity.isPositive())
            {
                removablePLISet.add(pli);
            }
            else
            {
                if(updateProductLineItemQuantity(pli,parsedQuantity))
                {
                    if (pli != null)
                    {
                        List<String> reasons = new ArrayList<String>(1);
                        reasons.add("InvalidQuantity");
                        adjustedItems.put(pli.getID(), reasons);
                    }
                }
            }
        }

        // If plis were found with quantity 0, remove them
        if (!removablePLISet.isEmpty())
        {
            try
            {
                basket.removeProductLineItemBOs(Iterators.createIterator(removablePLISet));
            }
            catch (RemoveException rex)
            {
                Logger.warn(this, "Exception occured while removing lineItems", rex );
            }
        }

        
        if( !adjustedItems.isEmpty() ){
            dict.put(DN_ADJUSTED_ITEMS, adjustedItems);
        }
        if( !rejectedItems.isEmpty() ){
            dict.put(DN_REJECTED_ITEMS, rejectedItems);
        }
        return PIPELET_NEXT;
    }
    

    /**
     * Checks if the adjust greater than MinOrderQuantity 
     * and (adjust/MinOrderQuantity) remainder is not zero
     * 
     * @param adjust
     * @param pli
     * @return true if MinOrderQuantity>adjust and (adjust/MinOrderQuantity) 
     *         the remainder is not zero, 
     *         otherwise false
     */
    private boolean isQuantityInvalid(Quantity adjust, ProductLineItemBO pli)
        throws QuantityException
    {
        // check if all the units match
        if(!adjust.isOfSameUnit(pli.getMinOrderQuantity()) || !adjust.isOfSameUnit(pli.getStepQuantity()))
        {
            throw new QuantityException("Quantities not of same unit.");
        }

        BigDecimal adjustValue = adjust.getValue();
        BigDecimal minValue    = pli.getMinOrderQuantity().getValue();
        BigDecimal stepValue   = pli.getStepQuantity().getValue();
        
        // if adjust = 0 return false
        if (adjustValue.compareTo(new BigDecimal("0.0")) <= 0  && removeNonPositiveConfigured)
        {
            return false;
        }

        // if min > adjust return true
        if (minValue.compareTo(adjustValue) == 1)
        {
            return true;
        }
        
        BigDecimal over         = adjustValue.subtract(minValue);
        int        highestScale = (over.scale() > stepValue.scale()) ? over.scale() : stepValue.scale();
        BigDecimal scaledOver   = over.movePointRight(highestScale);
        BigDecimal scaledStep   = stepValue.movePointRight(highestScale);
        BigInteger scaledRem    = scaledOver.unscaledValue().remainder(scaledStep.unscaledValue());
        BigDecimal remainder    = new BigDecimal(scaledRem).movePointLeft(highestScale);

        if (remainder.compareTo(new BigDecimal("0.0")) != 0)
        {
            return true;
        }
        
        return false;
    }

        
    /**
     * Updates the quantity and/or selected quantity of the given item by splitting it in case of attached 
     * gifting information.
     *
     * @param pli the item that should be updated
     * @param quantity the quantity the item should have
     * 
     * @return if the items quantity had to be adjusted due to minOrderQuantity/stepQuantity or splitting
     */
    protected boolean updateProductLineItemQuantity(ProductLineItemBO pli, Quantity quantity)
    {
        Collection<ProductLineItemBO> updatePLIs = new ArrayList<ProductLineItemBO>();
        boolean isQuantityAdjusted = false;

        // update ProductLineItemBO:Quantity ?
        if (updateQuantityConfigured) 
        {
            isQuantityAdjusted = pli.setQuantity(quantity, adjustQuantities);

            // split if pli contains gifting information
            if(pli.isGiftOptionMarked())
            {
                // create split plis from pli
//              workaround missing component
//                PLISplitInfoSplitStrategy splitStrategy = componentMgr.getComponent("QuantityPLISplitInfoSplitStrategy");
                PLISplitInfoSplitStrategy splitStrategy = componentMgr.getComponent("GiftingPLISplitInfoSplitStrategy");
                Collection<PLISplitInfo> splitPLIs = pli.getPLISplitInfo(splitStrategy);
                PLISplitInfoMap splitPLIsMap = new PLISplitInfoMapImpl();
                splitPLIsMap.put(pli, splitPLIs);
            
                // transform split plis to plis
                B2CBasketBO basket = (B2CBasketBO)pli.getLineItemCtnr();
//                workaround missing component
//                PLISplitInfoMergeStrategy mergeStrategy = componentMgr.getComponent("QuantityPLISplitInfoMergeStrategy");
                PLISplitInfoMergeStrategy mergeStrategy = componentMgr.getComponent("GiftingPLISplitInfoMergeStrategy");
                Map<PLISplitInfo, ProductLineItemBO> splitPLIsResultMap = basket.setPLISplitInfoMap(splitPLIsMap, mergeStrategy); 

                // create resulting collection of updated plis
                updatePLIs.addAll(splitPLIsResultMap.values());
            }
            // otherwise just set quantity
            else
            {
                updatePLIs.add(pli);
            }
        }
        else
        {
            updatePLIs.add(pli);
        }

        // update ProductLineItemBO:SelectedQuantity ? (not if pli was splitted)
        if (updateSelectedQuantityConfigured && updatePLIs.size() == 1) 
        {
            updatePLIs.iterator().next().setSelectedQuantity(quantity);
        }

        return isQuantityAdjusted || updatePLIs.size() > 1;
    }
    
}
